;; midi-time.scm - (c) rohan drape, 2002-2005

;; MIDI has a complicated notion of time.  

;; A midi file defines a DIVISION value that determines either: 1. the
;; number of MIDI DELTA TICKS in a quarter note or 2. the number of
;; MIDI DELTA TICKS per SMPTE frame, and the SMPTE format.

;; A TEMPO META-EVENT sets the number of microseconds (usecs) per MIDI
;; quarter note.

;; Some devices send a MIDI CLOCK event (status #xF8) at regular
;; intervals to set a tempo.  There are in all cases twenty-four MIDI
;; CLOCKS per quarter note.

;; Some devices send a MIDI TICK event (status #xF9) at regular
;; intervals of ten milliseconds.  There are therefore one hundred
;; MIDI TICKS arriving per second.

;; The SONG POSITION POINTER event sets the location in MIDI BEATS.
;; There are six MIDI CLOCKS per MIDI BEAT.  Thus a MIDI BEAT
;; represents a sixteenth note.

;; Predicates to determine the class of `division'.  This value is
;; stored at the start of a midi file.

(define (midi-division-is-metrical? division)
  (not (bit-set? 15 division)))

(define (midi-division-is-time-code? division)
  (bit-set? 15 division))

;; Iff `division' is SMPTE based return the format code, which should
;; be 24, 25, 29 or 30 (29 corresponds to 30 drop frame rate).

(define (midi-division-time-code-format division)
  (if (not (midi-division-is-time-code? division))
      (error "Division is not time code based" division))
  (- (expt 2 8) (arithmetic-shift division -8)))

;; Iff `division' is SMPTE based return the number of ticks per frame.

(define (midi-division-ticks-per-frame division)
  (if (not (midi-division-is-time-code? division))
      (error "Division is not time code based" division))
  (bitwise-and (- (expt 2 8) 1) division))

;; Convert the delta time `ticks' into seconds in relation to
;; `division' and 'tempo'.  The `tempo' is given in quarter notes per
;; minute.  The if statement is needed because the formula is
;; different for tracks based on metrical time and tracks based on
;; SMPTE time.

(define (midi-delta-ticks-to-seconds ticks division tempo)
  (if (midi-division-is-metrical? division)
      (let ((quarter-notes (/ ticks division))
	    (seconds-per-quarter-note (/ 60 tempo)))
	(* quarter-notes seconds-per-quarter-note))
      (let ((smpte-format (midi-division-time-code-format division))
	    (smpte-resolution (midi-division-ticks-per-frame division)))
	(/ ticks (* smpte-format smpte-resolution)))))

;; The inverse operation of 'midi-delta-ticks-to-seconds'.  The
;; result is rounded to the nearest exact integer.

(define (midi-seconds-to-delta-ticks seconds division tempo)
  (if (midi-division-is-metrical? division)
      (roundE (/ (* division seconds) (/ 60 tempo)))
      (let ((smpte-format (midi-division-time-code-format division))
	    (smpte-resolution (midi-division-ticks-per-frame division)))
	(roundE (* seconds smpte-format smpte-resolution)))))

;; This is trivial multiplication, and not valid for no-metrical
;; divisions.

(define (midi-pulses-to-delta-ticks pulses division)
  (if (midi-division-is-metrical? division)
      (roundE (* division pulses))
      (error "Division is not metrical")))

;; Convert the quarter notes per minute tempo `qpm' to the value
;; required by a TEMPO META EVENT.  This value is the number of
;; microseconds per MIDI quarter note.  The tempo value is sent/stored
;; as a twenty four bit value, ie as a sequence of three bytes.

(define (midi-qpm-to-upq qpm)
  (roundE (* (/ 60 qpm) 1000000)))

;; The inverse of 'midi-qpm-to-upq'.  This is in fact the same
;; operation, ie. the operation is symetric.  NOTE: This
;; implementation rounds to the nearest exact integer.  This is not
;; correct if the encoded tempo was not an integer, a rare but
;; important case.  Without the rounding however the encoding of the
;; 90 will be read as 89.999955.

(define (midi-upq-to-qpm tempo) (roundE (* (/ 60 tempo) 1000000)))
